import java.util.Random;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.math.Vector3;

/**
 * Board Class
 * game board the word games are played on
 * 
 * @author Zong Xiong
 */
public class Board extends GameObject{

/***************************************************
 * Constants
 ***************************************************/
	
	// pixel height, width, and border constants of tiles
	private static final float WIDTH_BORDER = 2;
	private static final float TILE_WIDTH = 40;
	private static final float HEIGHT_BORDER = 2;
	private static final float TILE_HEIGHT = 40;
	
	// size of the board in number of tiles
	private static final int ROW_SIZE = 9;
	private static final int COLUMN_SIZE = 7;
	private static final int NUM_TILES = ROW_SIZE * COLUMN_SIZE;
	
	// maximum word size for the board
	private static final int MAX_WORD_SIZE = 9;
	
	// constant used to weight certain letters for randomization
	private static final String LETTERS = "AAAAAAAAAEEEEEEEEEEEEIIIIIIIIIOOOOOOOONNNNNNRRRRRR" +
							 				"TTTTTTLLLLSSSSUUUUDDDDGGGBBCCMMPPFFHHVVWWYYKJXQZ";

/***************************************************
 * Fields
 ***************************************************/
	
	// stores tiles
	private Tile[][] tiles;
	// stores current word
	private String word;
	// stores dictionaries 
	private String[] threeDict, fourDict, fiveDict, sixDict, sevenDict, eightDict, nineDict;
	// stores points, number of attempts, number of active tiles, and combo score
	private int wordPoints = 0, attempts = -1, active_tiles = 14, combo;
	
	// used to randomize letters
	Random r = new Random();


/***************************************************
 * Public Methods
 ***************************************************/
	
	/**
	 * Default constructor
	 * creates a board with 14 tiles
	 * 
	 * @param x, lower-left x-position
	 * @param y, lower-left y-position
	 * @param inWidth, width of board
	 * @param inHeight, height of board
	 */
	public Board(float x, float y, float inWidth, float inHeight) {
		super(x, y, inWidth, inHeight);
		initialize();
		loadDictionaries();
		shuffle(active_tiles);
	}
	
	/**
	 * Constructor with specified tiles
	 * creates a board with a specified tile amount
	 * 
	 * @param x, lower-left x position
	 * @param y, lower-left y position
	 * @param inWidth, width of board
	 * @param inHeight, height of board
	 * @param num_tiles, number of tiles for board
	 */
	public Board(float x, float y, float inWidth, float inHeight, int num_tiles) {
		super(x, y, inWidth, inHeight);
		this.active_tiles = num_tiles;
		if (active_tiles>NUM_TILES) {
			active_tiles = NUM_TILES;
		}
		initialize();
		loadDictionaries();
		shuffle(active_tiles);
	}

	/**
	 * getTile
	 * Returns the tile at a particular index
	 * 
	 * @param index, index of the tile to return
	 * @return tile at index
	 */
	public Tile getTile(int index) {
		// checks if index is within tile range, if not returns null
		if (index >= 0 && index < NUM_TILES) {
			return tiles[index/COLUMN_SIZE][index%COLUMN_SIZE];
		}
		return null;
	}

	/**
	 * touchTileAt
	 * at the specified vector, we select or unselect a tile
	 * 
	 * @param pointer, specified vector
	 */
	public void touchTileAt(Vector3 pointer) {
		// find the row and column of the particular vector
		int row = (int) ((pointer.y - this.position.y) / (TILE_HEIGHT + HEIGHT_BORDER));
		int col = (int) ((pointer.x - this.position.x) / (TILE_WIDTH + WIDTH_BORDER));
		
		// checks if the tile is even visible, if so we check the tile
		if (this.tiles[row][col].isVisible()) {
			
			// check if tile isn't selected already and the current word is within bounds
			// if so we select the tile, and add the character to the word
			if (!this.tiles[row][col].isSelected() && this.word.length() < MAX_WORD_SIZE) {
				this.tiles[row][col].setSelected(true);
				this.word = this.word + this.tiles[row][col].getChar();
				this.wordPoints = this.wordPoints + this.tiles[row][col].getPointValue();
			}
			// check if tile is already selected, if so we remove the character for the word
			// and unselect the tile
			else if (this.tiles[row][col].isSelected()) {
				this.tiles[row][col].setSelected(false);
				this.word = removeLastChar(this.word, this.tiles[row][col].getChar());
				this.wordPoints = this.wordPoints - this.tiles[row][col].getPointValue();
			}
		}
	}

	/**
	 * getTileCount
	 * returns number of tiles that are active on the board
	 * 
	 * @return number of tiles active on the board
	 */
	public int getTileCount() {
		return active_tiles;
	}
	
	/**
	 * pop_word
	 * checks the word is a legal word in the dictionary, if so
	 * returns the score of the word, if not returns a negative
	 * number proportional to the number of attempts made
	 * 
	 * @return score of word or number of attempts
	 */
	public int pop_word() {
		// check if the word isn't null or length is greater than 3
		// also checks word in dictionary
		if (!this.word.equals("") && this.word.length() > 2 && checkWord()) {
			
			// we filter through the board, and find selected tiles, setting them
			// invisible, and re-adjusting the board
			for (int row=0; row < ROW_SIZE; row++) {
				for (int col=0; col < COLUMN_SIZE;) {
					if (this.tiles[row][col].isSelected()) {
						this.tiles[row][col].setSelected(false);
						this.tiles[row][col].setVisible(false);
						readjust_column(row, col);
					}
					else {
						col++;
					}
				}
			}
			
			// calculates word points
			int temp;
			combo++;
			temp = (wordPoints + this.word.length()-3);
			if (combo > 3) 
				temp = (int) (temp * (1.5 + (combo -3) * 0.25 ));
			if (this.isEmpty()) {
				int old_combo = combo;
				this.shuffle(14);
				combo = old_combo;
				temp = temp + 50;
			}
			
			// reset points, word, and attempts
			wordPoints = 0;
			this.word = "";
			attempts = -1;
			return temp;
		}
		
		// if the word is invalid, double the attempt value and return it
		attempts = attempts * 2;
		return attempts;
	}
	
	/**
	 * hasWord
	 * returns if the word isn't empty
	 * 
	 * @return true if the word isn't empty
	 */
	public boolean hasWord() {
		return this.word != "";
	}

	/**
	 * getWord
	 * retrives the word
	 * 
	 * @return the word on the board
	 */
	public String getWord() {
		return this.word;
	}
	
	/**
	 * isEmpty
	 * checks if the board is empty
	 * 
	 * @return true if board is empty
	 */
	public boolean isEmpty() {
		for (int row=0; row < ROW_SIZE; row++) {
			for (int col=0; col < COLUMN_SIZE; col++) {
				if (!this.tiles[row][col].isSelected()) {
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * shuffle
	 * randomizes tiles on board
	 * 
	 * @param Max_tiles, number of tiles to randomize
	 */
	public void shuffle(int Max_tiles) {
		active_tiles = 0;
		combo = 0;
		for (int row=0; row < ROW_SIZE && active_tiles < Max_tiles; row++) {
			for (int col=0; col < COLUMN_SIZE && active_tiles < Max_tiles; col++) {
				tiles[row][col].reset();
				tiles[row][col].setChar(LETTERS.charAt(r.nextInt(LETTERS.length())));
				active_tiles++;
			}
		}
	}
	
	/**
	 * addTiles
	 * attempts to add more tiles to the board
	 * 
	 * @param num_newTiles, number of new tiles to add
	 * @return false if the board was overfilled
	 */
	public boolean addTiles(int num_newTiles) {
		for (int row = 0; row < ROW_SIZE && num_newTiles > 0; row ++) {
			for (int col=0; col < COLUMN_SIZE && num_newTiles > 0; col++) {
				if (!tiles[row][col].isVisible()) {
					num_newTiles--;
					tiles[row][col].reset();
					tiles[row][col].setChar(LETTERS.charAt(r.nextInt(LETTERS.length())));
				}
			}
		}
		
		if (num_newTiles > 0) {
			return false;
		}
			
		return true;
	}
	
/***************************************************
 * Private Methods
 ***************************************************/
	
	/**
	 * initialize
	 * general initialization for a board
	 */
	private void initialize() {
		// blanks word
		this.word = "";
		
		// creates a blank board
		this.tiles = new Tile[ROW_SIZE][COLUMN_SIZE];
		for (int row = 0; row < ROW_SIZE; row++) {
			float tile_y_pos = this.position.y + (HEIGHT_BORDER + TILE_HEIGHT) * row;
			for (int col = 0; col < COLUMN_SIZE; col++) {
				float tile_x_pos = this.position.x + (WIDTH_BORDER + TILE_WIDTH) * col;
				tiles[row][col] = new Tile(tile_x_pos, tile_y_pos, TILE_WIDTH, TILE_HEIGHT, 'A');
				tiles[row][col].setVisible(false);
			}
		}
	}
	
	/**
	 * checkWord
	 * checks the board's current word
	 * 
	 * @return true if the word is a valid word
	 */
	private boolean checkWord() {
		// to improve speed, we separate the dictionaries by word length and for that
		// particular dictionary we search through it to see if the word is found.
		switch (word.length()) {
		case 3:
			for (int i = 0; i<threeDict.length; i++) {
				if (word.equals(threeDict[i])) {
					return true;
				}
			}
			break;
		
		case 4:
			for (int i = 0; i<fourDict.length; i++) {
				if (word.equals(fourDict[i])) {
					return true;
				}
			}
			break;

		case 5:
			for (int i = 0; i<fiveDict.length; i++) {
				if (word.equals(fiveDict[i])) {
					return true;
				}
			}
			break;

		case 6:
			for (int i = 0; i<sixDict.length; i++) {
				if (word.equals(sixDict[i])) {
					return true;
				}
			}
			break;

		case 7:
			for (int i = 0; i<sevenDict.length; i++) {
				if (word.equals(sevenDict[i])) {
					return true;
				}
			}
			break;
			
		case 8:
			for (int i = 0; i<eightDict.length; i++) {
				if (word.equals(eightDict[i])) {
					return true;
				}
			}
			
		case 9:
			for (int i = 0; i<nineDict.length; i++) {
				if (word.equals(nineDict[i])) {
					return true;
				}
			}
			
		}
		return false;
	}
	
	/**
	 * readjust_column
	 * readjusts a column when a tile is deleted, moving blocks
	 * downwards
	 * 
	 * @param row, row of tile to readjust
	 * @param col, col to readjust
	 */
	private void readjust_column(int row, int col) {
		if (row < ROW_SIZE-1) {
			if (this.tiles[row + 1][col].isVisible()) {
				this.tiles[row][col].setVisible(true);
				this.tiles[row][col].setSelected(this.tiles[row+1][col].isSelected());
				this.tiles[row][col].setChar(tiles[row+1][col].getChar().charAt(0));
				this.tiles[row+1][col].setVisible(false);
				readjust_column(row+1, col);
			}
		}	
	}
	
	/**
	 * loadDictionaries
	 * loads dictionaries for use
	 */
	private void loadDictionaries() {
		FileHandle file = Gdx.files.internal("data/3words.txt");
		threeDict = file.readString().split(" ");
		
		file = Gdx.files.internal("data/4words.txt");
		fourDict = file.readString().split(" ");

		file = Gdx.files.internal("data/5words.txt");
		fiveDict = file.readString().split(" ");
		
		file = Gdx.files.internal("data/6words.txt");
		sixDict = file.readString().split(" ");
		
		file = Gdx.files.internal("data/7words.txt");
		sevenDict = file.readString().split(" ");
		
		file = Gdx.files.internal("data/8words.txt");
		eightDict = file.readString().split(" ");
		
		file = Gdx.files.internal("data/9words.txt");
		nineDict = file.readString().split(" ");
	}
	
	/**
	 * removeLastChar
	 * removes the last instance of a character in a word
	 * 
	 * @param word, word to remove character from
	 * @param character, character to remove
	 * @return word with removed character
	 */
	private String removeLastChar(String word, String character) {
		// if the word length is 1, we remove empty the string
		if (word.length() == 1) {
			return "";
		}
		
		// else we create a string buffer, in which we load the word character by character
		// and skip the last iteration of the character specified, returning the result
		StringBuffer result = new StringBuffer(word.length()-1);
		boolean deleted = false;
		for (int i = word.length() - 1; i >= 0; i--) {
			if (!deleted && character.charAt(0) == word.charAt(i)) {
				deleted = true;
			}
			else {
				result.insert(0, word.charAt(i));
			}
		}
		return result.toString();
	}
}
